package cn.tedu.charging.cost.service.impl;

import cn.tedu.charging.cost.dao.repository.ChargingDataESRepository;
import cn.tedu.charging.cost.dao.repository.CostRuleCacheRepository;
import cn.tedu.charging.cost.dao.repository.CostRuleRepository;
import cn.tedu.charging.cost.pojo.ChargingData;
import cn.tedu.charging.cost.pojo.po.ChargingDataPO;
import cn.tedu.charging.cost.pojo.po.CostRulePO;
import cn.tedu.charing.common.pojo.param.ChargingProcessParam;
import cn.tedu.charing.common.pojo.vo.ChargingProcessVO;
import cn.tedu.charging.cost.service.CostService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.math.BigDecimal;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.*;

@Slf4j
@Service
public class CostServiceImpl implements CostService {

    /**
     * 全局变量的Map 用来记录充电次数
     * Key 订单号 value 实际充电的数据(次数,度数,花费)
     * 本地Map的问题
     *
     * 1 数据量问题
     * 计价服务 部署到服务器  业务服务的配置 4C/8G  8C/16G
     * 一直不重启 一直运行 运行了一月 100000个订单  每个订单平均很充满需要 2个小时  * 30s =
     *  1个小时  = 60分钟  1分钟 等于 60s  120个 * 1000 = 12000000 数据量大
     * 内存溢出 放不了大量的用户的充电数据
     *
     * 2 线程安全 多线程的充电数据安全问题
     * 3 持久化 重启后数据丢失,原来正常同步的数据丢失,从新开始计算
     *
     * 4 分布式 单机场景下 的业务解决方案 在分布式下,可能出现问题
     * 数据独享   计价服务部署了两台 4040 4041  负载均衡
     *  4040 本地map  4040访问不了4041的map
     *  4041 本地map  4041访问不了4040的map
     *
     * 数据共享 4040 能访问,4041也能访问
     *
     * 把充电数据 存储到 ES  4个问题 一次全部搞定
     *
     * 目标 计价服务要存储大量的数据 选择合适的存储方案 性能高,持久化,海量 存储的技术选型
     * Redis 分布式非关系型
     * 内存数据库  基于内存性能高,内存的容量有限,
     * 我的业务要求要持久化,Redis会导致性能下降
     *
     * Mysql 关系型数据库
     * 磁盘 严格 ACID mysql为了实现高严格的acid 性能相对较差
     * 数据量少 单机mysql
     * 数据量增加 分库分表...
     *
     * ES 分布式的搜索引擎 海量的存储 对ACID的实现没那么严格
     */
    private Map<String, ChargingData> chargingProcessData = new HashMap<>();


    /**
     * 计算一次价格
     *
     * 获取计价规则 通过场站id获取规则列表
     * 匹配计价规则
     *  获取当前时间(小时)
     *  通过当前时间(小时) 规则匹配 循环规则列表
     *  (判断当前小时 >= 规则的开始时间 and 判断当前小时 < 规则的结束时间)
     * 计算价格  = 通过设备同步的充电进度 乘 匹配的计价规则的每度的价格
     *
     * 价格的累加
     * 包含关系
     * 包含关系的度数
     * 累加的计算
     * 计算第几次  找个地方记下来 记哪?用什么记?
     *
     * @param chargingProcessParam
     * @return
     */
    @Override
    public ChargingProcessVO calculateCost(ChargingProcessParam chargingProcessParam) {

        //ChargingData chargingData = getChargingData(chargingProcessParam);

        ChargingDataPO chargingData = getChargingDataByES(chargingProcessParam);
        log.debug("通过订单号:{},获取充电数据:{},当前同步次数:{}",
                chargingProcessParam.getOrderNo(), chargingData, chargingData.getCount());

        Integer stationId = chargingProcessParam.getStationId();
        Integer gunId = chargingProcessParam.getGunId();
        //获取当前时间的小时
        Integer currentHour = getCurrentHour();
        log.debug("获取当前时间(小时):{}",currentHour);
        //通过当前时间小时和枪类型 获取 计价规则
        CostRulePO costRulePO = getMatchCostRuleByGunAndTime(stationId,gunId,currentHour);
        log.debug("获取当前时间(小时):{},匹配的计价规则:{}",currentHour,costRulePO);
        if (costRulePO != null) {
            //获取计价名称
            String name = costRulePO.getName();
            //获取每度电的价格
            BigDecimal powerFee = costRulePO.getPowerFee();
            //判断是否是第一次
            // 原因 第一次可以直接通过度数 * 电费 第二次需要计算度数(包含关系的度数) * 电费
            if (chargingData.isFirst()){
                //isFirst(chargingData.getCount())) {
                //第一次  度数 * 电费
                //2 two to   第一次计算的结果 to 放到 充电数据
                firstCost2ChargingData(chargingProcessParam,chargingData,name,powerFee,currentHour);
            }else {
                //不是第一次 计算度数  * 电费
                lastCost2ChargingData(chargingProcessParam,chargingData,name,powerFee,currentHour);
            }
        }else {
            //记录日志,方便快速定位问题 告警
            log.error("场站id:{},当前时间:{},获取计价规则失败,告警告警告警,接入告警系统",stationId,currentHour);
        }
        return buildChargingProcessVo(chargingData);
    }

    /**
     * 通过 充电数据构建 计价价格出参
     * @param chargingData
     * @return
     */
    private ChargingProcessVO buildChargingProcessVo(ChargingDataPO chargingData){
        ChargingProcessVO chargingProcessVO = new ChargingProcessVO();
        //参数少 可以通过手动的方式 设置
        chargingProcessVO.setOrderNo(chargingProcessVO.getOrderNo());
        //参数多,通过 工具 BeanUtils.copyProperties
        // 元数据 描述类 里面有几个字段,几个方法 的数据 叫做这个类的元数据
        // 1 获取 源(source) ChargingDataPO.class 获取 getter readMethod 读方法
        // 2 获取 目标(target)ChargingProcessVO.class 获取 setter writeMethod 写方法
        // 3 判断读方法和写方法的修饰符 如果不是 public setAccessible(true) 强制访问
        // 4 通过读方法 读取 source 的 值  Object value = readMethod.invoke(source);
        // 5 把source的值 value 通过 writeMethod 写入到target  writeMethod.invoke(target, value);
        BeanUtils.copyProperties(chargingData, chargingProcessVO);

        //计算充电时长 最后一次同步数据的时间 - 第一次同步数据的时间 充电时长
        //Date(不推荐) ,joda-time LocalDateTime (推荐)
        //LocalDateTime now = LocalDateTime.now();
        //Long startTime = chargingData.getStartTime();
        //now - startTime
        //Duration between = Duration.between(startTime, now);
        //between.toDays() // between
        return chargingProcessVO;
    }

    /**
     * 不是第一次计算电费
     * @param param
     * @param chargingData
     * @param name
     * @param powerFee
     * @param currentHour
     */
    private void lastCost2ChargingData(ChargingProcessParam param,ChargingDataPO chargingData,
                                       String name,BigDecimal powerFee,Integer currentHour) {

        log.debug("不是第一次同步充电数据");
        //获取充电度数
        //Float chargingCapacity = getRealChargingCapacity(param,chargingData);
        Float chargingCapacity = chargingData.getChargingCapacity(param.getChargingCapacity());
        //计算价格 充电度数 * 每度电的价格
        BigDecimal currentCost = getCost(chargingCapacity, powerFee);

        //保存当前的话费到充电数据,为了下次 同步数据的时候 做  <<<价格的累加>>> 准备
        //上次花费
        //BigDecimal lastTotalCost = chargingData.getTotalCost();
        //当前总花费 =  上次花费 + 本次花费
        //BigDecimal totalCost = lastTotalCost.add(currentCost);
        //保存当前总花费到充电数据  ChargingData
        chargingData.setTotalCost(currentCost);
        log.debug("当前时间(小时):{}," +
                        "计价规则名称:{}," +
                        "充电度数:{}," +
                        "电费单价:{}," +
                        "本次花费:{}," +
                        "总花费:{}",currentHour,name,chargingCapacity,
                powerFee,currentCost,chargingData.getTotalCost());
        //保存当前充电的度数到充电数据,设备同步的度数是<<<包含关系>>>
        // 为了下次 <<<计算真实充电度数>>> 做准备
        chargingData.setChargingCapacity(param.getChargingCapacity());
        //每次改变ChargingData,都需要把最新的充电数据保存到ES
        //save 不存在 保存,存在就通过id更新
        chargingDataESRepository.save(chargingData);
    }

    /**
     * 计算不是第一次的真实充电度数
     * 当前充电度数 减去 上次的充电度数
     * @return
     */
    private Float getRealChargingCapacity(ChargingProcessParam chargingProcessParam,ChargingData chargingData) {
        //当前充电度数
        Float currentChargingCapacity = chargingProcessParam.getChargingCapacity();
        //上次充电度数
        Float lastChargingCapacity = chargingData.getChargingCapacity();
        //计算真实度数  = 当前充电度数 - 上次充电度数
        return currentChargingCapacity - lastChargingCapacity;
    }

    /**
     * 第一次计算价格
     * @param param
     * @param chargingData
     * @param name
     * @param powerFee
     * @param currentHour
     */
    private void firstCost2ChargingData(ChargingProcessParam param,ChargingDataPO chargingData,
                                        String name,BigDecimal powerFee,Integer currentHour) {
        log.debug("第一次同步充电数据");
        //获取充电度数
        Float chargingCapacity = param.getChargingCapacity();
        //计算价格 充电度数 * 每度电的价格
        BigDecimal currentCost = getCost(chargingCapacity, powerFee);
        log.debug("当前时间(小时):{}," +
                "计价规则名称:{}," +
                "充电度数:{}," +
                "电费单价:{}," +
                "本次花费:{}," +
                "总花费:{}",currentHour,name,chargingCapacity,powerFee,currentCost,currentCost);
        //保存当前的话费到充电数据,为了下次 同步数据的时候 做  <<<价格的累加>>> 准备
        chargingData.setTotalCost(currentCost);
        //保存当前充电的度数到充电数据,设备同步的度数是<<<包含关系>>>
        // 为了下次 <<<计算真实充电度数>>> 做准备
        chargingData.setChargingCapacity(chargingCapacity);
        //每次改变ChargingData,都需要把最新的充电数据保存到ES
        //save 不存在 保存,存在就通过id更新
        chargingDataESRepository.save(chargingData);
    }

    /**
     * 判断同步的充电进度是否是第一次
     * @param count
     * @return
     */
    private Boolean isFirst(Integer count){
        return count == 1;
    }

    @Autowired
    private ChargingDataESRepository chargingDataESRepository;

    /**
     * 从ES获取充电数据
     * @param chargingProcessParam
     * @return
     */
    private ChargingDataPO getChargingDataByES(ChargingProcessParam chargingProcessParam) {
        //获取订单号
        String orderNo = chargingProcessParam.getOrderNo();
        //通过订单号去map里获取对应的充电数据
        Optional<ChargingDataPO> optional =
                chargingDataESRepository.findById(orderNo);
        /**
         * public boolean isPresent() {
         *         return value != null;
         *     }
         */
        if (optional.isPresent()) {
            //第N次 直接返回ES的数据
            ChargingDataPO chargingDataPO = optional.get();
            chargingDataPO.setCount(chargingDataPO.getCount() + 1);
            return chargingDataPO;
        }else {
            //充电数据为空 表示第一次同步,初始化一个充电数据
            ChargingDataPO chargingDataPO =
                    initESChargingData(chargingProcessParam);
            //记一下,存入ES
            chargingDataESRepository.save(chargingDataPO);
            return chargingDataPO;
        }
    }



    /**
     * 从本地Map获取充电数据
     * @param chargingProcessParam
     * @return
     */
    private ChargingData getChargingData(ChargingProcessParam chargingProcessParam) {
        //获取订单号
        String orderNo = chargingProcessParam.getOrderNo();
        //通过订单号去map里获取对应的充电数据
        ChargingData chargingData = chargingProcessData.get(orderNo);
        if (chargingData == null) {
            //充电数据为空 表示第一次同步,初始化一个充电数据,
            chargingData = initChargingData(chargingProcessParam);
            //记一下,存入map
            chargingProcessData.put(orderNo, chargingData);
        }else {
            //充电数据不为空 表示不是第一次同步
            //取上一次同步的次数
            Integer count = chargingData.getCount();
            //取上一次同步的次数 加 1 实现累加
            Integer newCount = count + 1;
            //把累加后的新次数 设置到本次充电数据
            chargingData.setCount(newCount);
            //再记一下 存入map
            chargingProcessData.put(orderNo,chargingData);
        }
        return chargingData;
    }

    /**
     * 初始化充电数据
     * @param chargingProcessParam
     * @return
     */
    private ChargingData initChargingData(ChargingProcessParam chargingProcessParam) {
        ChargingData chargingData = new ChargingData();
        chargingData.setOrderNo(chargingProcessParam.getOrderNo());
        chargingData.setGunId(chargingProcessParam.getGunId());
        chargingData.setUserId(chargingProcessParam.getUserId());
        chargingData.setStartTime(System.currentTimeMillis());
        chargingData.setCount(1);
        return chargingData;
    }

    /**
     * 初始化充电数据
     * @param chargingProcessParam
     * @return
     */
    private ChargingDataPO initESChargingData(ChargingProcessParam chargingProcessParam) {
        ChargingDataPO chargingData = new ChargingDataPO();
        //把要存到ES的充电数据的id 设置为订单号,后续方便通过id订单号查询
        chargingData.setId(chargingProcessParam.getOrderNo());
        chargingData.setOrderNo(chargingProcessParam.getOrderNo());
        chargingData.setGunId(chargingProcessParam.getGunId());
        chargingData.setUserId(chargingProcessParam.getUserId());
        chargingData.setStartTime(System.currentTimeMillis());
        chargingData.setCount(1);
        return chargingData;
    }


    /**
     * 计算价格 充电度数 * 每度电的价格
     * @param chargingCapacity
     * @param powerFee
     * @return
     */
    private BigDecimal getCost(Float chargingCapacity,BigDecimal powerFee) {

        //把Float类型的 chargingCapacity 转换为 String
        //String stringChargingCapacity = new String(chargingCapacity);
        //String stringChargingCapacity = String.valueOf(chargingCapacity);
        //String string = Float.toString(chargingCapacity);
        //把Float类型的 chargingCapacity 转换为 BigDecimal
        //通过构造器
        //BigDecimal bigDecimalFee = new BigDecimal(chargingCapacity);
        //valueOf()
        BigDecimal bigDecimalFee = BigDecimal.valueOf(chargingCapacity);
        return powerFee.multiply(bigDecimalFee);
    }

    /**
     * 获取当前时间 小时
     */
    private Integer getCurrentHour(){
        //当前时间
        LocalDateTime now = LocalDateTime.now();
        return now.getHour();
    }

    /**
     * 通过当前时间,去计价规则列表里匹配一条计价规则
     * @param stationId
     * @param gunId
     * @param hour
     * @return
     */
    private CostRulePO getMatchCostRuleByGunAndTime(Integer stationId,
                                                    Integer gunId,
                                                    Integer hour) {
        long start = System.currentTimeMillis();
        log.debug("通过站id:{},获取计价规则列表,开始时间:{}",stationId,start);
        List<CostRulePO> costRules = getCostRules(stationId);
        long end = System.currentTimeMillis();
        long cost = end - start ;
        log.debug("通过站id:{},获取计价规则列表:{}," +
                "开始时间:{},结束时间:{}," +
                "cost:{}", stationId, costRules,start,end,cost);
        //在多个规则列表里面,去挨个判断,找到一个匹配的规则 所以需要遍历规则列表
        for (CostRulePO costRule : costRules) {
            Integer startTime = costRule.getStartTime();
            Integer endTime = costRule.getEndTime();
            //当前时间是 10:33 小时  10
            //找真实的计价规则数据 通过人肉的方式 去判断 当前时间属于哪个时间段
            //属于 8,12  10 > 8 并且 10 < 12
            // hour > startTime && hour < endTime
            //边界值的处理 如果当前时间 8 如何处理 ? 如果当前时间是 12 如何处理
            // > 还是 >= 开始时间   < 还是 <= 结束时间
            // hour >= startTime && hour < endTime
            //8,12
            //14,18
            //18,24
            //12,14
            //0,8
            //8,12
            if (hour >= startTime && hour < endTime
                // 1 通过枪id 获取枪类型 2 入参直接传枪类型
                // && costRule.getGunType.equals("枪的具体类型")
            ) {
                return costRule;
            }
        }
        //走出循环,还没有return 表示 没有找到计价规则 返回null
        return null;
    }




    @Autowired
    private CostRuleRepository costRuleRepository;

    @Autowired
    private CostRuleCacheRepository costRuleCacheRepository;

    /**
     * 通过站id 获取 计价规则 列表
     * @param stationId
     * @return
     */
    private List<CostRulePO> getCostRules(Integer stationId){
        log.debug("通过场站id:{},从Redis查询计价规则",stationId);
        List<CostRulePO> cacheCostRules = costRuleCacheRepository.getCostRuleByStationId(stationId);
        log.debug("通过场站id:{},从Redis查询计价规则列表:{}",stationId,cacheCostRules);
        if (!CollectionUtils.isEmpty(cacheCostRules)){
            log.debug("通过场站id:{},从Redis查询计价规则列表," +
                    "Redis有数据,直接返回",stationId,cacheCostRules);
            return cacheCostRules;
        }else {
            log.debug("通过场站id:{},从Redis查询计价规则列表," +
                    "Redis没有数据,从数据库查询",stationId);
            List<CostRulePO> dbCostRules = costRuleRepository.getCostRules(stationId);
            log.debug("通过场站id:{},从Redis查询计价规则列表," +
                    "Redis没有数据,从数据库查询计价规则列表:{}",stationId,dbCostRules);
            if (!CollectionUtils.isEmpty(dbCostRules)){
                log.debug("通过场站id:{},从数据库查询计价规则列表," +
                        "数据库有数据,写入Redis,并且返回",stationId,cacheCostRules);
                costRuleCacheRepository.saveCostRules(stationId,dbCostRules);
                return dbCostRules;
            }
        }
        //return new ArrayList<>();
        return Collections.emptyList();
    }
}
